匹配几何效果(matchedGeometryEffect)

matchedGeometryEffect 用于在 不同视图之间建立几何关联关系,使视图在:

  • 位置变化
  • 尺寸变化
  • 布局层级变化
  • 条件渲染切换

这些场景中,仍然保持 连续、平滑、空间一致的动画过渡效果

该能力对应 SwiftUI 中的 matchedGeometryEffect,属于 组件级几何联动动画系统,不依赖导航系统。


一、API 定义

1matchedGeometryEffect?: {
2  id: string | number
3  namespace: NamespaceID
4  properties?: MatchedGeometryProperties
5  anchor?: Point | KeywordPoint
6  isSource?: boolean
7}
1type MatchedGeometryProperties = "frame" | "position" | "size"

二、核心作用

matchedGeometryEffect 的核心作用是:

让两个“逻辑上是同一个元素”的视图,在 不同布局结构中共享几何信息,从而产生连续的过渡动画。

它解决的问题包括:

  • 视图从一个容器移动到另一个容器时的“跳变”
  • 视图尺寸变化时的“突变”
  • 列表项展开为详情页时的“断层感”
  • Tab 切换指示器的“瞬移感”

三、参数详解

1. id(几何匹配唯一标识)

1id: string | number
  • 用于标识这是 哪一个几何元素

  • 在同一个 namespace 下:

    • id 相同的视图才会参与几何匹配
  • 通常来自:

    • 数据模型 ID
    • 索引值
    • 业务唯一标识

规则:

  • id 必须稳定

  • 动画期间不能频繁变化

  • 同一时刻:

    • 一个 id 只能有一个 isSource = true

2. namespace(几何命名空间)

1namespace: NamespaceID
  • 用于将多个匹配动画分组隔离

  • 不同 namespace 之间:

    • 即使 id 相同,也不会产生动画
  • 必须由 NamespaceReader 创建并注入

规则:

  • source 与 target 必须使用 同一个 namespace
  • 不允许跨 namespace 匹配

3. properties(参与匹配的几何属性)

1properties?: "frame" | "position" | "size"

默认值:

1properties = "frame"

含义说明:

含义
"frame" 同时匹配位置 + 尺寸
"position" 仅匹配中心点位置
"size" 仅匹配尺寸,不匹配位置

选择原则:

  • "frame":最完整、最自然的动画
  • "position":指示器、滑块、选中背景
  • "size":放大缩小、展开收起

4. anchor(锚点)

1anchor?: Point | KeywordPoint

默认值:

1anchor = "center"

作用:

  • 决定动画进行时:

    • 元素是从哪个相对位置进行对齐和计算的

常见取值:

  • "center"
  • "topLeading"
  • "topTrailing"
  • "bottomLeading"
  • "bottomTrailing"

使用场景:

  • 卡片从左上角展开
  • 头像从右上角放大
  • 底部元素向上弹出

5. isSource(是否作为几何数据的“源”)

1isSource?: boolean

默认值:

1isSource = true

含义说明:

行为
true 当前视图向外“提供”几何数据
false 当前视图“接收”几何动画结果

标准使用模式:

  • 原始视图:isSource = true
  • 目标视图:isSource = false

如果省略:

  • 第一个出现的视图默认作为 source
  • 其余作为接收方

四、最小可用示例(位置 + 尺寸联动)

该示例演示: 一个圆形在两个区域之间切换位置与尺寸,并保持连续动画。

1const expanded = useObservable(false)
2
3return <NamespaceReader>
4  {namespace => (
5    <VStack spacing={40}>
6      <Button
7        title="Toggle"
8        action={() => {
9          expanded.setValue( !expanded.value)
10        }}
11      />
12
13      <ZStack
14        frame={{ width: 300, height: 200 }}
15        background="systemGray6"
16      >
17        {!expanded.value && (
18          <Circle
19            fill="systemOrange"
20            frame={{ width: 60, height: 60 }}
21            matchedGeometryEffect={{
22              id: "circle",
23              namespace
24            }}
25          />
26        )}
27      </ZStack>
28
29      <ZStack
30        frame={{ width: 300, height: 300 }}
31        background="systemGray4"
32      >
33        {expanded.value && (
34          <Circle
35            fill="systemOrange"
36            frame={{ width: 150, height: 150 }}
37            matchedGeometryEffect={{
38              id: "circle",
39              namespace,
40              isSource: false
41            }}
42          />
43        )}
44      </ZStack>
45    </VStack>
46  )}
47</NamespaceReader>

该示例实现的动画效果:

  • 同一个圆:

    • 从上方小尺寸区域
    • 平滑移动并放大到下方大区域
  • 无跳变、无突变、无瞬移


五、仅同步“位置”的示例(指示器动画)

1const selected = useObservable(0)
2
3return <NamespaceReader>
4  {namespace => (
5    <HStack spacing={24}>
6      <Text
7        onTapGesture={() => selected.setValue(0)}
8        matchedGeometryEffect={{
9          id: "indicator",
10          namespace,
11          properties: "position",
12          isSource: selected.value === 0
13        }}
14      >
15        Tab 1
16      </Text>
17
18      <Text
19        onTapGesture={() => selected.setValue(1)}
20        matchedGeometryEffect={{
21          id: "indicator",
22          namespace,
23          properties: "position",
24          isSource: selected.value === 1
25        }}
26      >
27        Tab 2
28      </Text>
29    </HStack>
30  )}
31</NamespaceReader>

适用于:

  • Tab 选中动画
  • 滑块指示器
  • 选中背景平移

六、仅同步“尺寸”的示例(放大缩小)

1const expanded = useObservable(false)
2
3return <NamespaceReader>
4  {namespace => (
5    <ZStack>
6      <Circle
7        fill="systemBlue"
8        frame={{
9          width: expanded.value ? 200 : 80,
10          height: expanded.value ? 200 : 80
11        }}
12        matchedGeometryEffect={{
13          id: "avatar",
14          namespace,
15          properties: "size"
16        }}
17        onTapGesture={() => {
18          expanded.setValue(!expanded.value)
19        }}
20      />
21    </ZStack>
22  )}
23</NamespaceReader>

适用于:

  • 头像放大
  • 卡片展开
  • 按钮按压动画

七、多元素联动示例(卡片 → 详情)

1<NamespaceReader>
2  {namespace => (
3    <ZStack>
4      {!showDetail.value && (
5        <VStack spacing={16}>
6          <Image
7            source="cover"
8            matchedGeometryEffect={{
9              id: "card.image",
10              namespace
11            }}
12          />
13          <Text
14            matchedGeometryEffect={{
15              id: "card.title",
16              namespace
17            }}
18          >
19            Card Title
20          </Text>
21        </VStack>
22      )}
23
24      {showDetail.value && (
25        <VStack spacing={24}>
26          <Image
27            source="cover"
28            frame={{ width: 300, height: 200 }}
29            matchedGeometryEffect={{
30              id: "card.image",
31              namespace,
32              isSource: false
33            }}
34          />
35          <Text
36            font="largeTitle"
37            matchedGeometryEffect={{
38              id: "card.title",
39              namespace,
40              isSource: false
41            }}
42          >
43            Card Title
44          </Text>
45        </VStack>
46      )}
47    </ZStack>
48  )}
49</NamespaceReader>

效果说明:

  • 图片与标题同时参与几何匹配
  • 从卡片形态平滑过渡为详情页布局
  • 无需使用导航动画

八、关键使用规则总结

  1. namespace 必须完全相同

  2. id 必须完全一致

  3. 同一时刻:

    • 一个 id 只能有一个 isSource = true
  4. 默认行为:

    1properties = "frame"
    2anchor = "center"
    3isSource = true
  5. source 与 target 必须:

    • 同一渲染周期内完成切换
  6. 如果 source 和 target:

    • 同时存在,且都为 isSource = true → 动画不确定,可能失效
  7. Widget 与 Live Activity 环境不支持完整 matchedGeometry 动画能力


九、适用场景总结

适合使用 matchedGeometryEffect 的场景:

  • Tab 指示器动画
  • 卡片 → 详情展开
  • 图片放大预览
  • 列表项选中动画
  • 分栏布局中的选中项切换

不适合使用的场景:

  • 高频数据刷新列表
  • 大量同时进行几何动画的复杂视图树
  • 帧率敏感的实时图表